06 集合与迭代
> Last Format Time:8/11/2026 21:45:38
本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。
当前前端开发关键要点
> [!important] 学习优先级:P1 > - 数组等可迭代对象使用 for...of;普通对象键枚举使用 Object.keys 等 API,谨慎使用 for...in。 > - 需要任意类型键时使用 Map,需要唯一值集合时使用 Set;不要只为语法新颖而替换简单对象或数组。 > - 理解 iterable、iterator、类数组与展开语法的关系,能判断一个值为何可以被遍历。 > - 了解 WeakMap、WeakSet 的弱引用特点及其不可枚举限制。
知识点导航
[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/for...in与for...of](./06 集合与迭代.md)
- JS中 for...in 和 for...of 的核心区别(超清晰版)
- 一句话总结
- 遍历的内容不同(最核心区别)
- for...in
- for...of
- 适用数据类型不同
- for...in
- for...of
- 遍历数组时的表现差异(重点)
- 支持的语法特性
- 最佳使用场景(背会就不会用错)
- ✅ 用 for...in 的场景
- ✅ 用 for...of 的场景
- 极简记忆口诀
- 总结
[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/可迭代对象](./06 集合与迭代.md)
- 可迭代对象
[现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/类数组](./06 集合与迭代.md)
- 类数组
[现代JS教程与阮一峰ES6 · 5特殊内建对象/Map和Set](./06 集合与迭代.md)
- JavaScript中Map的实现原理
- 核心实现:哈希表
- 关键技术点与优化
- 哈希冲突处理
- 键的多样性支持
- 迭代顺序维护
- 总结:实现效果的原因
- 与Object的对比
- Map Set 映射和集合
- Map
- Set
- 弱映射和弱集合
- Set 的基本特性
- Set 的常用方法与属性
- 基本操作
- 示例:
- 遍历方法
- 示例:
- Set 的典型应用场景
- 与数组的区别
- 总结
[廖雪峰教程 · 3快速入门/3.8 Map与Set](./06 集合与迭代.md)
- Map与Set
- Map
- Set
[廖雪峰教程 · 3快速入门/3.9 iterable](./06 集合与迭代.md)
- iterable
现代前端补充与纠错
> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。
> [!warning] 下方保留旧教程上下文;迭代器代码已直接修正,集合复杂度和弱集合规则已就地标成 【修正】。
正确的可迭代对象
【修正】 return 后不能换行再写对象,否则自动分号插入会返回 undefined;循环变量也应显式声明:
【修正代码】
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const item of range) {
console.log(item);
}
Map、Set 与弱集合
- 【补充】 规范只要求
Map的平均访问时间优于线性,不保证具体实现一定是哈希表或严格O(1);复杂度结论应结合引擎和数据规模。 - 【补充】
Map适合任意键和频繁增删;普通对象更适合固定字段记录。不要仅凭“性能更快”做选择。 - 【修正】 当前规范中
WeakMap/WeakSet的弱持有键可为对象或未注册的 Symbol;字符串、数字等原始值仍不可用。 - 【补充】 弱集合不可枚举是可达性与垃圾回收语义的一部分,不是功能缺失。
旧笔记知识全集(按来源保留)
现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/可迭代对象
可迭代对象
==实现了 [Symbol.iterator] 属性==的对象即是 Iterable 对象,然后可以使用操作符 for...of 进行迭代
为了让对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内建 symbol)。
- 当
for..of循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有next方法的对象。 - 从此开始,
for..of仅适用于这个被返回的对象。 - 当
for..of循环希望取得下一个数值,它就调用这个对象的next()方法。 next()方法返回的结果的==格式必须是{done: Boolean, value: any}==,当done=true时,表示循环结束,否则value是下一个值。
let data = {
name: 'dano',
age: 21,
eat: true,
from: 1,
to: 5,
}
data[Symbol.iterator] = function () {
return {
name: this.name,
age: this.age,
current: this.from,
last: this.to,
next() {
if (this.current <= this.last)
{
return {
done: false,
value: this.current++,
}
}
else
{
return {
done: true,
}
}
},
}
}
for (const item of data) {
console.log(item);
}
8/11/26 【修正代码】原内容:return 后换行再写对象,且 item 未声明;修改点:避免 ASI 返回 undefined,并改为 for (const item of data)。
上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
字符串是一个类似数组的对象,也原生具有 Iterator 接口。
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
Symbol.iterator()方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]
for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
entries()返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。keys()返回一个遍历器对象,用来遍历所有的键名。values()返回一个遍历器对象,用来遍历所有的键值。
这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。
let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']
现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/类数组
类数组
在 JavaScript 中,类数组(Array-like) 或类数组对象(Array-like Object) 是指那些==外观和行为类似数组==,但本质上不是 Array 实例的对象。
它们的核心特征是:
- 具有 ==
length属性==(表示元素数量); - 可以通过==索引(如
0、1、2)访问元素==; - 但==不具备
Array原型==方法(如push、map、forEach等)。
常见的类数组对象
arguments对象 函数内部的局部变量,包含函数调用时传递的所有参数。
function sum() {
console.log(arguments); // [1, 2, 3](类数组形式)
console.log(arguments.length); // 3
console.log(arguments[0]); // 1(通过索引访问)
}
sum(1, 2, 3);
- DOM 集合 如
document.getElementsByTagName()、document.querySelectorAll()返回的集合(HTMLCollection或NodeList)。
const divs = document.getElementsByTagName('div');
console.log(divs.length); // 页面中 div 的数量
console.log(divs[0]); // 第一个 div 元素
- 字符串(
String) 字符串可以通过索引访问字符,且有length属性,也被视为类数组。
const str = "hello";
console.log(str.length); // 5
console.log(str[0]); // "h"
- 自定义类数组对象 手动定义的具有
length和索引属性的对象。
const arrLike = {
0: "a",
1: "b",
2: "c",
length: 3
};
类数组与数组的区别
| 特性 | 类数组对象 | 数组(Array) |
|---|---|---|
| 原型链 | 原型通常是 Object.prototype | 原型是 Array.prototype |
| 数组方法 | 不具备(如 map、forEach 等) | 具备所有数组方法 |
instanceof Array | 返回 false | 返回 true |
类数组转换为真正的数组 由于类数组不支持数组方法,实际开发中常需要将其转换为真正的数组,以便使用 map、filter 等方法。常用转换方式:
Array.from()(推荐,ES6+) 专门用于将类数组或可迭代对象转换为数组。
const arrLike = { 0: 1, 1: 2, 2: 3, length: 3 };
const arr = Array.from(arrLike);
console.log(arr); // [1, 2, 3]
console.log(arr.map(x => x * 2)); // [2, 4, 6](可使用数组方法)
- 扩展运算符(
...)(ES6+,==仅适用于可迭代对象==) 如NodeList、字符串等可迭代的类数组。
const divs = document.querySelectorAll('div');
const divArray = [...divs]; // 转换为数组
const str = "hello";
const strArray = [...str]; // ["h", "e", "l", "l", "o"]
Array.prototype.slice.call()(传统方法) 利用slice方法的特性转换(slice不传参数时返回原数组的副本)。
const argumentsArr = Array.prototype.slice.call(arguments);
// 等价于:[].slice.call(arguments)
总结
- 类数组对象是“长得像数组”的对象,核心特征是
length属性和索引访问; - 常见于
arguments、DOM 集合、字符串等; - 转换为真正的数组后,才能使用
Array的所有方法,常用Array.from()或扩展运算符实现转换。
理解类数组的特性有助于在处理 DOM 操作、函数参数等场景时避免错误(如直接调用数组方法导致的报错)。
有==索引和 length 属性 ==、=> 类数组对象
let arrayLike = { // 有索引和 length 属性 => 类数组对象
0: "Hello",
1: "World",
length: 2
};
// Error (no Symbol.iterator)
for (let item of arrayLike) {}
有一个全局方法 Array.from 可以接受一个==可迭代或类数组==的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。
现代JS教程与阮一峰ES6 · 1数据类型与变量/Object/for...in与for...of
JS中 for...in 和 for...of 的核心区别(超清晰版)
这是 JavaScript 面试和日常开发高频考点,我用最简单、最实用的方式给你讲清楚,一看就懂。
一句话总结
for...in:遍历键名(key/index),适合遍历对象,不适合遍历数组for...of:遍历值(value),适合遍历数组/类数组/可迭代对象,ES6 新增
遍历的内容不同(最核心区别)
for...in
遍历键名(对象的 key、数组的 index)
const arr = ['a', 'b', 'c'];
for (let index in arr) {
console.log(index); // 输出 0, 1, 2 (字符串类型)
}
for...of
遍历值(数组的元素、迭代器返回的值)
const arr = ['a', 'b', 'c'];
for (let value of arr) {
console.log(value); // 输出 a, b, c
}
适用数据类型不同
for...in
可以遍历对象(普通对象、数组都能用)
const obj = { name: '小明', age: 18 };
for (let key in obj) {
console.log(key, obj[key]); // name 小明,age 18
}
⚠️ 缺点:会遍历出原型链上的属性,容易出问题。
for...of
不能直接遍历普通对象,只能遍历可迭代对象:
- 数组
- 字符串
- Map / Set
- arguments、DOM 集合
- Generator 等
// 遍历字符串
for (let char of 'hello') {
console.log(char); // h e l l o
}
遍历数组时的表现差异(重点)
| 特性 | for...in | for...of |
|---|---|---|
| 遍历内容 | 数组下标(字符串类型) | 数组元素 |
| 顺序 | 不保证顺序 | 严格按数组顺序 |
| 原型属性 | 会遍历出来 | 不会 |
| 数组新增方法 | 会遍历到 | 不会 |
例子:for...in 会踩坑
Array.prototype.test = '123';
const arr = [10, 20];
for (let index in arr) {
console.log(index);
// 输出 0、1、test ❌ 把原型上的属性也遍历了
}
for (let value of arr) {
console.log(value);
// 输出 10、20 ✅ 安全、干净
}
支持的语法特性
for...in:不支持await(异步遍历)for...of:支持await(非常实用)
// 异步遍历(for...of 独有)
async function test() {
const arr = [1, 2, 3];
for (let item of arr) {
await new Promise(r => setTimeout(r, 1000));
console.log(item);
}
}
最佳使用场景(背会就不会用错)
✅ 用 for...in 的场景
- 遍历普通对象(
{})获取键值对
✅ 用 for...of 的场景
- 遍历数组
- 遍历字符串
- 遍历 Map / Set
- 异步遍历
- 需要直接拿元素值的时候
极简记忆口诀
> 看键用 in,看值用 of;对象用 in,数组用 of。
总结
- for...in 遍历键/下标,适合对象,会遍历原型属性,不推荐遍历数组
- for...of 遍历值/元素,适合数组/字符串/可迭代对象,安全、现代、推荐
- 遍历数组优先用 for...of,遍历普通对象用 for...in
现代JS教程与阮一峰ES6 · 5特殊内建对象/Map和Set
JavaScript中Map的实现原理
核心实现:哈希表
8/11/26 【修正】原内容:JavaScript Map 的底层必然是哈希表并保证平均 O(1);修改点:规范只要求平均访问时间优于线性,具体哈希表、树或混合结构属于引擎实现。以下段落仅作为一种可能实现理解。 哈希表是实现 Map 理想的数据结构,因为它能提供非常高效的查找、插入和删除操作,平均时间复杂度接近 O(1)。
为什么哈希表能实现这个效果? 它的核心思想是使用一个哈希函数 (Hash Function) 将一个键 (key) 转换成一个数字(哈希值),然后将这个数字映射到内部数组的一个索引上。数据就存储在这个数组对应的位置。
基本工作流程
- 设置值 (
set(key, value)):- 对键
key应用哈希函数,计算出一个哈希值。 - 将这个哈希值通过某种运算(比如取模
%)转换到内部数组的一个索引index。 - 将键值对存储到数组的
index位置。
- 对键
- 获取值 (
get(key)):- 对同一个键
key再次应用相同的哈希函数,得到相同的哈希值。 - 计算出相同的索引
index。 - 直接去数组的
index位置读取数据。
- 对同一个键
正因为通过索引直接访问数组元素是极快的,所以 Map 的操作才如此高效。
关键技术点与优化
单纯的哈希表会遇到一些问题,JS 引擎的实现中会使用高级技术来解决它们。
哈希冲突处理
当两个不同的键经过哈希函数计算后得到了相同的数组索引,就发生了哈希冲突。Map 必须有能力处理这种情况。
解决方案
最常见的解决方案是链地址法 (Separate Chaining)。数组的每一个位置不再直接存储一个键值对,而是存储一个链表(或数组)。当发生冲突时,新的键值对会被添加到这个索引对应的链表中。
- 查找时:先通过索引找到链表,然后再遍历这个短链表来找到准确的键(使用严格相等
===进行比较)。 - 优化:为了保持高效,引擎会监控哈希表的"负载因子"(已用位置/总位置)。当负载因子过高时,会执行"扩容"(Rehashing),即创建一个更大的新数组,并重新计算所有键的哈希值和新索引,然后将数据迁移过去。这虽然是一次昂贵的操作,但平均摊还下来,依然能保持 O(1) 的时间复杂度。
键的多样性支持
与普通对象(Object)只能使用字符串或 Symbol 作为键不同,Map 的键可以是任何类型,包括对象、函数、数字、字符串等。
实现方式
引擎会为每个键(尤其是对象这样的引用类型)生成一个唯一的标识符或内部哈希码,用于计算哈希值。这个细节对 JavaScript 开发者是完全透明的。
迭代顺序维护
Map 的一个关键特性是它保持键的插入顺序。当你用 for...of 循环遍历 Map 时,得到的顺序与你插入 set 的顺序一致。
实现方式
单纯的哈希表本身不记录插入顺序。为了实现这个特性,引擎在哈希表之外,还会维护一个双向链表(或类似的有序数据结构)来记录键值对的插入顺序。
- 当你插入一个新的键值对时,它既会被放入哈希表中,也会被追加到链表的末尾。
- 当你遍历
Map时,引擎实际上是在遍历这个链表,从而保证了顺序。 - 删除操作也需要同时从哈希表和链表中移除该项。
这是一种典型的空间换时间的策略,用额外的内存开销来换取确定的迭代顺序。
总结:实现效果的原因
Map 能实现哈希表的效果是因为:
- 基于哈希函数: 使用哈希函数将键快速映射到内存地址,实现了平均 O(1) 的访问速度。
- 处理冲突: 使用链地址法等技术妥善处理哈希冲突,保证数据的正确性。
- 动态扩容: 在数据量增加时自动扩容,维持高效的性能。
- 维护顺序: 通过额外的数据结构(如链表)来维护插入顺序,满足了 ECMAScript 规范的要求。
- 引擎优化: 像 V8 这样的引擎还会根据
Map的大小和操作类型进行多种优化(例如,对于小Map可能使用不同的底层表示),但这些实现细节对开发者是隐藏的。
与Object的对比
| 特性 | Map | Object |
|---|---|---|
| 键类型 | 任意值 | String 或 Symbol |
| 迭代顺序 | 可靠的插入顺序 | 现代规范定义了属性顺序,但整数索引、字符串键和 Symbol 键分组规则不同 |
| 大小获取 | size 属性(O(1)) | 手动计算(O(n)) |
| 性能 | 在频繁增删键值对的场景下优化得更好 | 未针对频繁增删进行特殊优化 |
| 默认原型 | 无 | 有,可能意外地从原型链上继承到键名 |
结论
JavaScript 的 Map 是一种高级抽象,其底层核心是哈希表,并辅以链表等数据结构来处理冲突和维护顺序。正是这种实现使得它具备了快速查找和有序迭代的强大特性。
Map Set 映射和集合
Map
是一个带键的数据项的集合,就像一个 Object 一样。但是它们最大的差别是 Map 允许任何类型的键(key)。 它的方法和属性如下:
new Map([[键,值],[键,值]])—— 创建 map。map.set(key, value)—— 根据键存储值。map.get(key)—— 根据键来返回值,如果map中不存在对应的key,则返回undefined。map.has(key)—— 如果key存在则返回true,否则返回false。map.delete(key)—— 删除指定键的值。map.clear()—— 清空 map。map.size—— 返回当前元素个数。 每一次map.set调用都会返回 map 本身,所以我们可以进行“链式”调用
如果要在 map 里使用循环,可以使用以下三个方法:
map.keys()—— 遍历并返回一个包含所有键的可迭代对象,map.values()—— 遍历并返回一个包含所有值的可迭代对象,map.entries()—— 遍历并返回一个包含所有实体[key, value]的可迭代对象,for..of在默认情况下使用的就是这个。
从对象创建Map:
let obj = {
name: "dano",
age: 21,
};
let map = new Map(Object.entries(obj));
console.log(map);//Map(2) { 'name' => 'dano', 'age' => 21 }
从Map创建对象:
let prices = Object.fromEntries([
['shitr', 2],
['apple', 23],
['fruit',43]
])
console.log(prices);//{ shitr: 2, apple: 23, fruit: 43 }
Set
Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。 它的主要方法如下:
new Set(iterable)—— 创建一个set,如果提供了一个iterable对象(通常是数组),将会从数组里面复制值到set中。set.add(value)—— 添加一个值,返回 set 本身set.delete(value)—— 删除值,如果value在这个方法调用的时候存在则返回true,否则返回false。set.has(value)—— 如果value在 set 中,返回true,否则返回false。set.clear()—— 清空 set。set.size—— 返回元素个数。
我们有客人来访,我们想记住他们每一个人。但是已经来访过的客人再次来访,不应造成重复记录。每个访客必须只被“计数”一次。
let set = new Set();
set.add({ name: 'dano' }).add({ name: 'jungle' });
console.log(set.size);//2
for (let value of set) {
console.log(value.name);
//dano jungle
}
注意一件有趣的事儿。forEach 的回调函数有三个参数:一个 value,然后是 同一个值 valueAgain,最后是目标对象。没错,同一个值在参数里出现了两次。
forEach 的回调函数有三个参数,是为了与 Map 兼容。当然,这看起来确实有些奇怪。但是这对在特定情况下轻松地用 Set 代替 Map 很有帮助,反之亦然。
Map 中用于迭代的方法在 Set 中也同样支持:
set.keys()—— 遍历并返回一个包含所有值的可迭代对象,set.values()—— 与set.keys()作用相同,这是为了兼容Map,set.entries()—— 遍历并返回一个包含所有的实体[value, value]的可迭代对象,它的存在也是为了兼容Map。
弱映射和弱集合
8/11/26 【修正】原内容:WeakMap 的键必须是对象;修改点:当前规范还允许未注册的 Symbol,其他原始值仍不可用。WeakMap 的键可以是对象或未注册的 Symbol。 如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。也就是说WeakMap并不能阻止对象被回收,而普通的Map可以。
let obj = { name: 'dano' };
//let map = new Map().set(obj, "shit");
let weakmap = new WeakMap().set(obj, "shit2");
obj = null;
//console.log(map.get(obj));
console.log(weakmap.get(obj));
WeakMap 的主要应用场景是 额外数据的存储。
假如我们正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象共存亡 —— 这时候 WeakMap 正是我们所需要的利器。
我们将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除。
WeakSet 的表现类似: 8/11/26 【修正】原内容:WeakSet 只能添加对象;修改点:当前规范还允许未注册的 Symbol。
- 与
Set类似,但只能添加对象或未注册的 Symbol。 - 对象只有在其它某个(些)地方能被访问的时候,才能留在
WeakSet中。 - 跟
Set一样,WeakSet支持add,has和delete方法,但不支持size和keys(),并且不可迭代。
WeakSet 是类似于 Set 的集合,可弱持有对象或未注册的 Symbol;当对象键通过其他方式不可达时,垃圾回收器可以回收它。
在 JavaScript 中,Set 是 ES6 引入的一种集合数据结构,用于存储唯一的值(不允许重复)。它类似于数组,但成员的值都是唯一的,没有重复项。Set 本身是一个构造函数,用于创建 Set 实例。
Set 的基本特性
- 值的唯一性:
Set中的每个值都是唯一的,无论添加多少次相同的值,最终只会保留一个。 (判断唯一性的方式类似===,但特殊地,NaN被视为与自身相等,这与===不同)
8/11/26 【修正】原内容:Set 无序;修改点:Set 没有数字下标,但迭代遵循插入顺序。 2. 插入顺序:Set 没有数组式数字索引,但会按插入顺序迭代。
- 可迭代性:
Set是可迭代对象,可通过for...of循环遍历,也支持扩展运算符(...)。
Set 的常用方法与属性
基本操作
new Set([iterable]):创建Set实例,可选参数为可迭代对象(如数组),会自动去重。add(value):添加值,返回Set本身(可链式调用)。delete(value):删除值,返回布尔值(是否删除成功)。has(value):判断是否包含某个值,返回布尔值。clear():清空所有元素,无返回值。size:属性,返回元素个数(类似数组的length)。
示例:
// 创建 Set(自动去重)
const set = new Set([1, 2, 2, 3, 3, 3]);
console.log(set); // Set(3) {1, 2, 3}
console.log(set.size); // 3
// 添加元素
set.add(4).add(5); // 链式调用
console.log(set); // Set(5) {1, 2, 3, 4, 5}
// 判断是否包含
console.log(set.has(3)); // true
console.log(set.has(6)); // false
// 删除元素
set.delete(2);
console.log(set); // Set(4) {1, 3, 4, 5}
// 清空
set.clear();
console.log(set.size); // 0
遍历方法
Set 提供了多种遍历方式,由于元素无序,遍历顺序与插入顺序一致:
keys():返回键名的迭代器(Set的键名与键值相同)。values():返回键值的迭代器(常用)。entries():返回[value, value]的迭代器(因为键名与键值相同)。forEach(callback):使用回调函数遍历每个元素。
示例:
const set = new Set(['a', 'b', 'c']);
// for...of 遍历 values()
for (const value of set.values()) {
console.log(value); // 'a' 'b' 'c'
}
// forEach 遍历
set.forEach((value) => {
console.log(value); // 'a' 'b' 'c'
});
// 扩展运算符转为数组
const arr = [...set];
console.log(arr); // ['a', 'b', 'c']
Set 的典型应用场景
- 数组去重:利用
Set的唯一性快速去重。
const arr = [1, 2, 2, 3, 3, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]
判断元素是否存在:
has方法的性能优于数组的indexOf或includes(尤其数据量大时)。存储不重复的集合数据:如用户ID、标签等需要唯一标识的数据。
实现交集、并集、差集:
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// 并集
const union = new Set([...setA, ...setB]); // {1, 2, 3, 4}
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x))); // {2, 3}
// 差集(setA 有而 setB 没有)
const difference = new Set([...setA].filter(x => !setB.has(x))); // {1}
与数组的区别
| 特性 | Set | 数组(Array) |
|---|---|---|
| 元素唯一性 | 自动去重,无重复值 | 允许重复值 |
| 索引访问 | 无索引,无法通过下标访问 | 有索引,可通过 [index] 访问 |
| 常用操作 | 增删查效率高(哈希表实现) | 增删查效率受位置影响 |
| 适用场景 | 存储唯一值、集合运算 | 有序数据、需要索引访问 |
总结
Set 是一种高效的集合数据结构,核心优势是自动去重和快速的增删查操作。它弥补了传统数组在处理“唯一值集合”时的不足,常用于数组去重、集合运算等场景。在需要存储不重复数据且无需索引的场景中,Set 是比数组更合适的选择。
廖雪峰教程 · 3快速入门/3.8 Map与Set
Map与Set
JavaScript的默认对象表示方式{}可以视为其他语言中的Map或Dictionary的数据结构,即一组键值对。
但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。
Map
Map是一组键值对的结构,具有极快的查找速度。
举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array:
let names = ['Michael', 'Bob', 'Tracy'];
let scores = [95, 75, 85];
给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。
如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用JavaScript写一个Map如下:
let m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
初始化Map需要一个二维数组,或者直接初始化一个空Map。Map具有以下方法:
let m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined
由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:
let m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88
Set
Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。
要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set:
let s1 = new Set(); // 空Set
let s2 = new Set([1, 2, 3]); // 含1, 2, 3
重复元素在Set中自动被过滤:
let s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}
通过delete(key)方法可以删除元素:
let s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}
廖雪峰教程 · 3快速入门/3.9 iterable
iterable
遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。
具有iterable类型的集合可以通过新的for ... of循环来遍历。用法如下:
let a = ['A', 'B', 'C'];
let s = new Set(['A', 'B', 'C']);
let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (let x of a) { // 遍历Array
console.log(x);
}
for (let x of s) { // 遍历Set
console.log(x);
}
for (let x of m) { // 遍历Map
console.log(x[0] + '=' + x[1]);
}
你可能会有疑问,for ... of循环和for ... in循环有何区别?
for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。
当我们手动给Array对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:
let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x in a) {
console.log(x); // '0', '1', '2', 'name'
}
for ... in循环将把name包括在内,但Array的length属性却不包括在内。
for ... of循环则完全修复了这些问题,它只循环集合本身的元素:
let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x of a) {
console.log(x); // 'A', 'B', 'C'
}
这就是为什么要引入新的for ... of循环。
然而,更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。
注意,forEach()方法是ES5.1标准引入的,你需要测试浏览器是否支持。
Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
let s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
console.log(element);
});
Map的回调函数参数依次为value、key和map本身:
let m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
console.log(value);
});
如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Array的element:
let a = ['A', 'B', 'C'];
a.forEach(function (element) {
console.log(element);
});